package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.Feign.ResponseMappingDecoder;
import io.kiki.stack.http.feign.Logger.NoOpLogger;
import io.kiki.stack.http.feign.Request.Options;
import io.kiki.stack.http.feign.codec.Decoder;
import io.kiki.stack.http.feign.codec.Encoder;
import io.kiki.stack.http.feign.codec.ErrorDecoder;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static io.kiki.stack.http.feign.ExceptionPropagationPolicy.NONE;

public abstract class BaseBuilder<B extends BaseBuilder<B, T>, T> implements Cloneable {

    protected final List<RequestInterceptor> requestInterceptors = new ArrayList<>();
    protected final List<ResponseInterceptor> responseInterceptors = new ArrayList<>();
    private final B thisB;
    protected Logger.Level logLevel = Logger.Level.NONE;
    protected Contract contract = new Contract.Default();
    protected Retryer retryer = new Retryer.Default();
    protected Logger logger = new NoOpLogger();
    protected Encoder encoder = new Encoder.Default();
    protected Decoder decoder = new Decoder.Default();
    protected boolean closeAfterDecode = true;
    protected boolean decodeVoid = false;
    protected QueryMapEncoder queryMapEncoder = QueryMap.MapEncoder.FIELD.instance();
    protected ErrorDecoder errorDecoder = new ErrorDecoder.Default();
    protected Options options = new Options();
    protected InvocationHandlerFactory invocationHandlerFactory = new DefaultInvocationHandlerFactory();

    protected ExceptionPropagationPolicy propagationPolicy = NONE;
    protected List<Capability> capabilities = new ArrayList<>();

    public BaseBuilder() {
        super();
        thisB = (B) this;
    }

    public B logLevel(Logger.Level logLevel) {
        this.logLevel = logLevel;
        return thisB;
    }

    public B contract(Contract contract) {
        this.contract = contract;
        return thisB;
    }

    public B retryer(Retryer retryer) {
        this.retryer = retryer;
        return thisB;
    }

    public B logger(Logger logger) {
        this.logger = logger;
        return thisB;
    }

    public B encoder(Encoder encoder) {
        this.encoder = encoder;
        return thisB;
    }

    public B decoder(Decoder decoder) {
        this.decoder = decoder;
        return thisB;
    }

    /**
     * This flag indicates that the response should not be automatically closed upon completion of
     * decoding the message. This should be set if you plan on processing the response into a
     * lazy-evaluated construct, such as a {@link java.util.Iterator}. Feign standard decoders do not
     * have built in support for this flag. If you are using this flag, you MUST also use a custom
     * Decoder, and be sure to close all resources appropriately somewhere in the Decoder (you can use
     * {@link Util#ensureClosed} for convenience).
     *
     * @since 9.6
     */
    public B doNotCloseAfterDecode() {
        this.closeAfterDecode = false;
        return thisB;
    }

    public B decodeVoid() {
        this.decodeVoid = true;
        return thisB;
    }

    public B queryMapEncoder(QueryMapEncoder queryMapEncoder) {
        this.queryMapEncoder = queryMapEncoder;
        return thisB;
    }

    /**
     * Allows to map the response before passing it to the decoder.
     */
    public B mapAndDecode(ResponseMapper mapper, Decoder decoder) {
        this.decoder = new ResponseMappingDecoder(mapper, decoder);
        return thisB;
    }


    /**
     * This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
     * 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
     *
     * <p>All first-party (ex gson) decoders return well-known empty values defined by {@link
     * Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder) decoder} or
     * make your own.
     *
     * <p>This flag only works with 404, as opposed to all or arbitrary status codes. This was an
     * explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
     * fallback policy. If your server returns a different status for not-found, correct via a custom
     * {@link #client(Client) client}.
     *
     * @since 8.12
     * @deprecated use {@link #dismiss404()} instead.
     */
    @Deprecated
    public B decode404() {
        this.dismiss404 = true;
        return thisB;
    }

    public B errorDecoder(ErrorDecoder errorDecoder) {
        this.errorDecoder = errorDecoder;
        return thisB;
    }

    public B options(Options options) {
        this.options = options;
        return thisB;
    }

    /**
     * Adds a single request interceptor to the builder.
     */
    public B requestInterceptor(RequestInterceptor requestInterceptor) {
        this.requestInterceptors.add(requestInterceptor);
        return thisB;
    }

    /**
     * Sets the full set of request interceptors for the builder, overwriting any previous
     * interceptors.
     */
    public B requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
        this.requestInterceptors.clear();
        for (RequestInterceptor requestInterceptor : requestInterceptors) {
            this.requestInterceptors.add(requestInterceptor);
        }
        return thisB;
    }

    /**
     * Sets the full set of request interceptors for the builder, overwriting any previous
     * interceptors.
     */
    public B responseInterceptors(Iterable<ResponseInterceptor> responseInterceptors) {
        this.responseInterceptors.clear();
        for (ResponseInterceptor responseInterceptor : responseInterceptors) {
            this.responseInterceptors.add(responseInterceptor);
        }
        return thisB;
    }

    /**
     * Adds a single response interceptor to the builder.
     */
    public B responseInterceptor(ResponseInterceptor responseInterceptor) {
        this.responseInterceptors.add(responseInterceptor);
        return thisB;
    }

    /**
     * Allows you to override how reflective dispatch works inside of Feign.
     */
    public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
        this.invocationHandlerFactory = invocationHandlerFactory;
        return thisB;
    }

    public B exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
        this.propagationPolicy = propagationPolicy;
        return thisB;
    }

    public B addCapability(Capability capability) {
        this.capabilities.add(capability);
        return thisB;
    }

    @SuppressWarnings("unchecked")
    B enrich() {
        if (capabilities.isEmpty()) {
            return thisB;
        }

        try {
            B clone = (B) thisB.clone();

            getFieldsToEnrich().forEach(field -> {
                field.setAccessible(true);
                try {
                    final Object originalValue = field.get(clone);
                    final Object enriched;
                    if (originalValue instanceof List) {
                        Type ownerType = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
                        enriched = ((List) originalValue).stream().map(value -> Capability.enrich(value, (Class<?>) ownerType, capabilities)).collect(Collectors.toList());
                    } else {
                        enriched = Capability.enrich(originalValue, field.getType(), capabilities);
                    }
                    field.set(clone, enriched);
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new RuntimeException("Unable to enrich field " + field, e);
                } finally {
                    field.setAccessible(false);
                }
            });

            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    List<Field> getFieldsToEnrich() {
        return Util.allFields(getClass()).stream()
                // exclude anything generated by compiler
                .filter(field -> !field.isSynthetic())
                // and capabilities itself
                .filter(field -> !Objects.equals(field.getName(), "capabilities"))
                // and thisB helper field
                .filter(field -> !Objects.equals(field.getName(), "thisB"))
                // skip primitive types
                .filter(field -> !field.getType().isPrimitive())
                // skip enumerations
                .filter(field -> !field.getType().isEnum()).collect(Collectors.toList());
    }

    public final T build() {
        return enrich().internalBuild();
    }

    protected abstract T internalBuild();

    protected ResponseInterceptorChain responseInterceptorChain() {
        ResponseInterceptorChain endOfResponseInterceptorChain = ResponseInterceptorChain.DEFAULT;
        ResponseInterceptorChain executionResponseInterceptorChain = this.responseInterceptors.stream().reduce(ResponseInterceptor::andThen).map(interceptor -> interceptor.apply(endOfResponseInterceptorChain)).orElse(endOfResponseInterceptorChain);

        return (ResponseInterceptorChain) Capability.enrich(executionResponseInterceptorChain, ResponseInterceptorChain.class, capabilities);
    }
}
